// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.scene;

import bats.Assets;
import bats.map.WaterControl;
import com.jme3.material.Material;
import com.jme3.math.Plane;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.WaterProcessor;
import jme3utilities.sky.SkyControl;
import jme3utilities.sky.Updater;

/**
 * A scene component to manage water features.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Water {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Water.class.getName());
    // *************************************************************************
    // fields
    /**
     * map surface elevations to water processors
     */
    final private Map<Float, WaterProcessor> processors = new TreeMap<>();
    /**
     * attachment point for water features
     */
    final private Node features = new Node("water features");
    /**
     * scene subtree for spatials which are visible in reflections
     */
    final private Node reflectibles = new Node("reflectibles");
    /**
     * scene in which the water features reside: set by constructor
     */
    final private SceneNode scene;
    // *************************************************************************
    // constructors

    /**
     * Instantiate for a specified scene.
     *
     * @param scene in which the water features will reside (not null)
     */
    Water(SceneNode scene) {
        assert scene != null;
        this.scene = scene;
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Add a sky control to the scene.
     *
     * @param control which control to add (not null)
     */
    void addSkyControl(SkyControl control) {
        assert control != null;

        reflectibles.addControl(control);

        Updater updater = control.getUpdater();
        for (WaterProcessor processor : processors.values()) {
            processor.addListener(updater);
        }
    }

    /**
     * Adopt an (already parented) geometry as a water feature.
     *
     * @param geometry which geometry to adopt (not null)
     */
    public void adoptAsFeature(Geometry geometry) {
        if (geometry == null) {
            throw new NullPointerException("geometry cannot be null");
        }
        if (MySpatial.isOrphan(geometry)) {
            throw new IllegalArgumentException("geometry must be parented");
        }

        Node oldParent = geometry.getParent();
        MySpatial.adopt(features, geometry);
        /*
         * Add a control to keep the geometry positioned to match
         * its former parent.
         */
        WaterControl control = new WaterControl(oldParent);
        geometry.addControl(control);
    }

    /**
     * Attach a spatial to the reflectibles node.
     *
     * @param spatial which spatial to attach (not null)
     */
    void attachReflectible(Spatial spatial) {
        assert spatial != null;
        reflectibles.attachChild(spatial);
    }

    /**
     * Find the water feature controlled by the specified node.
     *
     * @param controllingNode the controlling node (not null)
     * @return the pre-existing water feature (or null if there's none)
     */
    Spatial getWaterFeature(Node controllingNode) {
        assert controllingNode != null;

        for (Spatial child : features.getChildren()) {
            WaterControl waterControl = child.getControl(WaterControl.class);
            assert waterControl != null;
            if (waterControl.getControllingNode() == controllingNode) {
                return child;
            }
        }
        return null;
    }

    /**
     * Initialize this instance, configuring its nodes and attaching them to the
     * scene's top node.
     */
    void initialize() {
        features.setCullHint(Spatial.CullHint.Dynamic);
        scene.attachChild(features);

        reflectibles.setCullHint(Spatial.CullHint.Dynamic);
        scene.attachChild(reflectibles);
    }

    /**
     * Apply water material to a horizontal surface.
     *
     * @param surface which water feature to apply to (not null)
     */
    public void setMaterial(Geometry surface) {
        assert surface != null;

        float surfaceElevation = MySpatial.getYLevel(surface); // world units
        Material material = getMaterial(surfaceElevation);
        surface.setMaterial(material);
    }
    // *************************************************************************
    // private methods

    /**
     * Find the water material for a specified surface elevation. If no water
     * processor exists for that elevation, create one.
     *
     * @param surfaceY world elevation of the water's surface (in world units)
     */
    private Material getMaterial(float surfaceElevation) {
        WaterProcessor processor = processors.get(surfaceElevation);
        if (processor == null) {
            /*
             * Create a water processor for this elevation.
             */
            float scaleFactor = scene.getScaleFactor();
            processor = Assets.createWater(scaleFactor);
            Lighting lighting = scene.getLighting();
            processor.addListener(lighting);
            Plane surface = new Plane(Vector3f.UNIT_Y, surfaceElevation);
            processor.setPlane(surface);
            processor.setReflectionScene(reflectibles);

            scene.getViewPort().addProcessor(processor);
            processors.put(surfaceElevation, processor);
        }
        Material result = processor.getMaterial();
        return result;
    }
}